/*
 * File     : InitialisationOption.java
 * Created  : 16 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni;

import java.util.Collections;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * <p>
 *  Defines constants for options for initialising DNI.
 * </p>
 *
 * @author Matthew Wilson
 */
@SuppressFBWarnings( value = "NAB_NEEDLESS_BOOLEAN_CONSTANT_CONVERSION",
                     justification = "False positive" )
public enum InitialisationOption
{

    // Implementation note:
    // This enum class itself must NOT cause any of the rest of DNI to load; in
    // particular, it must not initialise DirectNativeInterface.

    /**
     * <p>
     *  Maps to a boolean value indicating whether to enable debug mode.
     * </p>
     * <p>
     *  When enabled, further internal checks are made to reduce programming
     *  errors made by users of this toolkit.
     * </p>
     */
    DEBUG_MODE( Boolean.class, false ),

    /**
     * <p>
     *  Maps to a boolean value indicating whether to enable debug mode for
     *  allocating native memory.
     * </p>
     * <p>
     *  When enabled, further internal checks are made to reduce programming
     *  errors made by users of this toolkit when allocating and freeing memory.
     *  Using this option will increase the Java heap usage and decrease
     *  performance significantly.
     * </p>
     */
    ALLOCATOR_DEBUG_MODE( Boolean.class, false ),

    /**
     * Maps to a boolean value indicating whether to attempt to use
     * <code>sun.misc.Unsafe</code>.
     */
    USE_SUN_MISC_UNSAFE( Boolean.class, true ),

    // everything uses byte-code generation...
//    /**
//     * <p>
//     *  Maps to a boolean value indicating whether byte-code generation is used
//     *  for library and structure instances.
//     * </p>
//     * <p>
//     *  When enabled, the a class loader with the interface's class loader as
//     *  parent is used to load custom implementations.
//     * </p>
//     */
//    BYTE_CODE_GENERATION_MODE( Boolean.class, true ),

    /**
     * <p>
     *  Maps to a String value indicating where byte-code should be dumped.
     * </p>
     * <p>
     *  When set to a non-null, non-empty value, the directory will be created
     *  and byte-code for generated classes placed there.
     * </p>
     */
    DUMP_BYTE_CODE( String.class, null ),

    /**
     * <p>
     *  Maps to a String value defining the {@link Allocator} class name of an
     *  alternative implementation to be used.
     * </p>
     * <p>
     *  When set to a non-null, non-empty value, the class is loaded with the
     *  thread's context class loader, and an instance created using a default,
     *  no-arg constructor.
     * </p>
     */
    ALLOCATOR( String.class, null ),

    /**
     * <p>
     *  Maps to a integer value defining how much scratch memory is used per
     *  thread for native calls.
     * </p>
     * <p>
     *  Native calls often require per-call scratch memory (for example, when
     *  encoding {@link String}s into a native form).  This value sets the
     *  initial amount of memory allocated, in KiB.  The default is 256KiB.
     * </p>
     */
    SCRATCH_MEMORY_SIZE( Integer.class, 256 ),

    ;

    private final Class< ? > type;

    private final Object defaultValue;

    /**
     * @param type
     *            the type
     * @param defaultValue
     *            the default value
     */
    InitialisationOption( final Class< ? > type, final Object defaultValue )
    {
        this.type = type;
        this.defaultValue = type.cast( defaultValue );
    }

    /**
     * <p>
     *  Obtains the system default from the system properties set.
     * </p>
     *
     * @return the system default value
     */
    public Object getSystemDefault()
    {
        String propertyName = getPropertyName();

        String value = System.getProperty( propertyName );
        if ( value == null )
        {
            return this.defaultValue;
        }

        try
        {
            return this.type.getConstructor( String.class ).newInstance( value );
        }
        catch ( Exception exception )
        {
            return this.defaultValue;
        }
    }

    /**
     * @return the System property name for this option
     */
    public String getPropertyName()
    {
        return "dni.".concat( name() ).replace( '_', '.' ).toLowerCase( Locale.US );
    }

    /**
     * <p>
     *  Provides initialisation support for DNI.
     * </p>
     *
     * @author Matthew Wilson
     */
    public static final class Initialisation
    {
        private static final AtomicBoolean INITIALISED = new AtomicBoolean();

        private static final AtomicReference< Thread > INITIALIASER_THREAD = new AtomicReference< Thread >();

        private static Map< InitialisationOption, Object > initOptions;

        /** Prevents instantiation. */
        private Initialisation()
        {
            throw new UnsupportedOperationException();
        }

        /**
         * <p>
         *  Initialises DNI with the given options.
         * </p>
         * <p>
         *  This method must be called at most once, and must be called
         *  <em>before</em> the {@link DirectNativeInterface} class has been
         *  initialised.
         * </p>
         * <p>
         *  It is only necessary to call this method if it is not possible to
         *  set the options via system properties.
         * </p>
         *
         * @param options
         *            the initialisation options
         */
        public static void init( final Map< InitialisationOption, ? > options )
        {
            if ( !INITIALISED.compareAndSet( false, true ) )
            {
                throw new IllegalStateException( "Already initialised" );
            }
            if ( !INITIALIASER_THREAD.compareAndSet( null, Thread.currentThread() ) )
            {
                throw new IllegalStateException( "Initialising from two places at once" );
            }

            initOptions = copy( options );

            // force DNI to be initialised now
            DirectNativeInterface.init();
        }

        /**
         * Called by {@link DirectNativeInterface} when it is initialised.
         * @return the map of options
         */
        static Map< InitialisationOption, Object > getInitialisationOptions()
        {
            if ( INITIALISED.getAndSet( true ) && !INITIALIASER_THREAD.compareAndSet( Thread.currentThread(), null ) )
            {
                throw new IllegalStateException( "Initialising from two places at once" );
            }

            Map< InitialisationOption, Object > options = initOptions;
            if ( options == null )
            {
                return copy( Collections.< InitialisationOption, Object >emptyMap() );
            }

            // no longer needed by this class
            initOptions = null;

            return options;
        }

        /**
         * Copies a user-provided map, filling in missing values.
         *
         * @param options
         *            the user-provided options
         * @return options
         */
        private static Map< InitialisationOption, Object > copy( final Map< InitialisationOption, ? > options )
        {
            Map< InitialisationOption, Object > copy = new EnumMap< InitialisationOption, Object >( InitialisationOption.class );
            for ( InitialisationOption option : InitialisationOption.values() )
            {
                Object value = options.get( option );
                if ( value == null )
                {
                    value = option.getSystemDefault();
                }
                copy.put( option, value );
            }
            return copy;
        }
    }

}

